STM32 的 CAN 特性及架构

STM32 CAN 控制器

STM32 自带的是 bxCAN,即基本扩展 CAN。它支持 CAN 协议 2.0A 和 2.0B。它的设计目标是,以最小的 CPU 负荷来高效处理大量收到的报文。它也支持报文发送的优先级要求(优先级特性可软件配置)。对于安全紧要的应用, bxCAN 提供所有支持时间触发通信模式所需的硬件功能。

STM32 的 bxCAN 的主要特点有:

  • 支持 CAN 协议 2.0A 和 2.0B 主动模式
  • 波特率最高达 1Mbps
  • 支持时间触发通信
  • 具有 3 个发送邮箱
  • 具有 3 级深度的 2 个接收 FIFO
  • 可变的过滤器组(最多 28 个)

STM32 的标识符过滤是一个比较复杂的东东,它的存在减少了 CPU 处理 CAN 通信的开销。STM32 的过滤器组最多有 28 个(互联型),但是 STM32F103ZET6 只有 14 个(增强型),每个滤波器组 x 由2个 32 位寄存器, CAN_FxR1 和 CAN_FxR2 组成。

STM32 每个过滤器组的位宽都可以独立配置,以满足应用程序的不同需求。根据位宽的不
同,每个过滤器组可提供:

  • 1 个 32 位过滤器,包括: STDID[10:0]、 EXTID[17:0]、 IDE 和 RTR 位
  • 2 个 16 位过滤器,包括: STDID[10:0]、 IDE、 RTR 和 EXTID[17:15]位
    此外过滤器可配置为,屏蔽位模式和标识符列表模式

为了过滤出一组标识符,应该设置过滤器组工作在屏蔽位模式。
为了过滤出一个标识符,应该设置过滤器组工作在标识符列表模式。

CAN 架构

Tx Mailboxes(发送邮箱)
STM32 的 CAN 中共有 3 个发送邮箱供软件来发送报文。发送调度器根据优先级决定
哪个邮箱的报文先被发送。

Accepttance Filters( 接收过滤器 )
STM32 的 CAN 中共有 14 个位宽可变/可配置的标识符过滤器组,软件通过对它们编
程,从而在 CAN 收到的报文中选择它需要的报文,而把其它报文丢弃掉。

Receive FIFO( 接收 FIFO )
STM32 的 CAN 中共有 2 个接收 FIFO,每个 FIFO 都可以存放 3 个完整的报文。它们
完全由硬件来管理。

CAN 的工作就是围绕这三部分展开的

发送流程

CAN 发送流程为:程序选择 1 个空置的邮箱( TME=1)–> 设置标识符( ID),数据长度和发送数据–>设置 CAN_TIxR 的 TXRQ 位为 1,请求发送–> 邮箱挂号(等待成为最高优先级) –> 预定发送(等待总线空闲)–> 发送 –> 邮箱空置。

CAN 接收流程为:

FIFO 空 –> 收到有效报文 –> 挂号_1(存入 FIFO 的一个邮箱,这个由硬件
控制,我们不需要理会) –>收到有效报文–> 挂号_2 –>收到有效报文 –>挂号_3–>收到有效报文
溢出。

CAN 的位时序

由发送单元在非同步的情况下发送的每秒钟的位数称为位速率。一个位可分为 4 段。

  • 同步段( SS)
  • 传播时间段( PTS)
  • 相位缓冲段 1( PBS1)
  • 相位缓冲段 2( PBS2)
    这些段又由可称为 Time Quantum(以下称为 Tq)的最小时间单位构成。

1 位分为 4 个段,每个段又由若干个 Tq 构成,这称为位时序。1 位由多少个 Tq 构成、每个段又由多少个 Tq 构成等,可以任意设定位时序。通过设定位时序,多个单元可同时采样,也可任意设定采样点。

采样点,是指读取总线电平,并将读到的电平作为位值的点。位置在 PBS1 结束处。根据这个位时序,我们就可以计算 CAN 通信的波特率了。

在总线空闲态,最先开始发送消息的单元获得发送权。当多个单元同时开始发送时,各发送单元从仲裁段的第一位开始进行仲裁。连续输出显性电平最多的单元可继续发送。


STM32 CAN 位时序特性

STM32 把传播时间段和相位缓冲段 1( STM32 称之为时间段 1)合并了,所以 STM32 的 CAN 一个位只有 3 段:同步段( SYNC_SEG)、时间段 1( BS1)和时间段2( BS2)。 STM32 的 BS1 段可以设置为 1~16 个时间单元,刚好等于我们上面介绍的传播时间段和相位缓冲段 1 之和。

CAN 波特率的计算公式,我们只需要知道 BS1 和 BS2 的设置,以及 APB1
的时钟频率(一般为 36Mhz),就可以方便的计算出波特率。比如设置 TS1=6、 TS2=7 和 BRP=4,
在 APB1 频率为 36Mhz 的条件下,即可得到 CAN 通信的波特率=36000/[(7+8+1) * 5]=450Kbps。

CAN 的主控制寄存器 CAN_MCR

CAN 的初始化配置步骤

CAN 相关的固件库函数和定义分布在文件 stm32f10x_can.c 和头文件 stm32f10x_can.h 文件中。

1)配置相关引脚的复用功能,使能 CAN 时钟

2)设置 CAN 工作模式及波特率等。
这一步通过先设置 CAN_MCR 寄存器的 INRQ 位,让 CAN 进入初始化模式,然后设置
CAN_MCR 的其他相关控制位。再通过 CAN_BTR 设置波特率和工作模式(正常模式/环回模式)
等信息。 最后设置 INRQ 为 0,退出初始化模式。

库函数中,提供了函数 CAN_Init()用来初始化 CAN 的工作模式以及波特率, CAN_Init()函数体中,在初始化之前,会设置 CAN_MCR 寄存器的 INRQ 为 1 让其进入初始化模式,然后初始化 CAN_MCR 寄存器和 CRN_BTR 寄存器之后,会设置 CAN_MCR 寄存器的 INRQ 为 0让其退出初始化模式。所以我们在调用这个函数的前后不需要再进行初始化模式设置。

初始化实例为:

1
2
3
4
5
6
7
8
9
10
11
12
13
CAN_InitStructure.CAN_TTCM=DISABLE; //非时间触发通信模式
CAN_InitStructure.CAN_ABOM=DISABLE; //软件自动离线管理
CAN_InitStructure.CAN_AWUM=DISABLE; //睡眠模式通过软件唤醒
CAN_InitStructure.CAN_NART=ENABLE; //禁止报文自动传送
CAN_InitStructure.CAN_RFLM=DISABLE; //报文不锁定,新的覆盖旧的
CAN_InitStructure.CAN_TXFP=DISABLE; //优先级由报文标识符决定
CAN_InitStructure.CAN_Mode= CAN_Mode_LoopBack; //模式设置: 1,回环模式;
//设置波特率
CAN_InitStructure.CAN_SJW=CAN_SJW_1tq;//重新同步跳跃宽度为个时间单位
CAN_InitStructure.CAN_BS1=CAN_BS1_8tq; //时间段 1 占用 8 个时间单位
CAN_InitStructure.CAN_BS2=CAN_BS2_7tq;//时间段 2 占用 7 个时间单位
CAN_InitStructure.CAN_Prescaler=5; //分频系数(Fdiv)
CAN_Init(CAN1, &CAN_InitStructure); // 初始化 CAN1

3)设置滤波器。
先设置 CAN_FMR的 FINIT 位,让过滤器组工作在初始化模式下,然后设置滤波器组 0 的工作模式以及标识符 ID
和屏蔽位。最后激活滤波器,并退出滤波器初始化模式。

过滤器初始化参考实例代码:

1
2
3
4
5
6
7
8
9
10
CAN_FilterInitStructure.CAN_FilterNumber=0; //过滤器 0
CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask;
CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit; //32 位
CAN_FilterInitStructure.CAN_FilterIdHigh=0x0000;////32 位 ID
CAN_FilterInitStructure.CAN_FilterIdLow=0x0000;
CAN_FilterInitStructure.CAN_FilterMaskIdHigh=0x0000;//32 位 MASK
CAN_FilterInitStructure.CAN_FilterMaskIdLow=0x0000;
CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0;// FIFO0
CAN_FilterInitStructure.CAN_FilterActivation=ENABLE; //激活过滤器 0
CAN_FilterInit(&CAN_FilterInitStructure);//滤波器初始化

至此, CAN 就可以开始正常工作了。如果用到中断,就还需要进行中断相关的配置,

4)发送接受消息

在初始化 CAN 相关参数以及过滤器之后,接下来就是发送和接收消息了。 库函数中提供
了发送和接受消息的函数。

发送消息的函数是:

1
uint8_t CAN_Transmit(CAN_TypeDef* CANx, CanTxMsg* TxMessage);

第一个参数是 CAN 标号,我们使用 CAN1。第二个参数是相关消息结构
体 CanTxMsg 指针类型, CanTxMsg 结构体的成员变量用来设置标准标识符,扩展标示符,消
息类型和消息帧长度等信息。

接受消息的函数是:

1
void CAN_Receive(CAN_TypeDef* CANx, uint8_t FIFONumber, CanRxMsg* RxMessage;

前面两个参数也比较好理解, CAN 标号和 FIFO 号。 第二个参数 RxMessage 是用来存放接受到
的消息信息。

结构体 CanRxMsg 和结构体 CanTxMsg 比较接近,分别用来定义发送消息和描述接受消息,

5) CAN 状态获取
对于 CAN 发送消息的状态,挂起消息数目等等之类的传输状态信息,库函数提供了一些列的函数,包括 CAN_TransmitStatus()函数, CAN_MessagePending()函数, CAN_GetFlagStatus()函数等等,大家可以根据需要来调用。

CAN 的中断由发送中断、接收 FIFO 中断和错误中断构成。发送中断由三个发送邮箱
任意一个为空的事件构成。接收 FIFO 中断分为 FIFO0 和 FIFO1 的中断,接收 FIFO 收到
新的报文或报文溢出的事件可以引起中断。本实验中使用的 RX0 中断通道即为 FIFO0 中
断通道,当 FIFO0 收到新报文时,引起中断,我们就在相应的中断服务函数读取这个新报

打包报文

配置好 CAN 接口后,我们就可以复用它来发送数据了。利用 CAN 发送数据,要先把数据打包成完整的 CAN 报文格式。

void CAN_SetMsg(void)
{
//TxMessage.StdId=0x00;
TxMessage.ExtId=0x1314; //使用的扩展 ID
TxMessage.IDE=CAN_ID_EXT; //扩展模式
TxMessage.RTR=CAN_RTR_DATA; //发送的是数据
TxMessage.DLC=2; //数据长度为 2 字节
TxMessage.Data[0]=0xAB;
TxMessage.Data[1]=0xCD;
}

函数使用的结构体变量 TxMessage 在 main 文件以全局变量的形式定义,结构体变量 TxMessage 和 RxMessage.

1
2
CanTxMsg TxMessage; // 发送缓冲区
CanRxMsg RxMessage; // 接收缓冲区

TxMessage 的类型为 CanTxMsg,而接收报文时,我们使用 CanRxMsg 类型。它们都是由库文件定义的结构体类型。

报文打包函数

1
2
3
4
5
6
7
8
9
10
void CAN_SetMsg(void)
{
//TxMessage.StdId=0x00;
TxMessage.ExtId=0x1314; //使用的扩展 ID
TxMessage.IDE=CAN_ID_EXT; //扩展模式
TxMessage.RTR=CAN_RTR_DATA; //发送的是数据
TxMessage.DLC=2; //数据长度为 2 字节
TxMessage.Data[0]=0xDC;
TxMessage.Data[1]=0xBA;
}

接收报文结构体 CanRxMsg

1
2
3
4
5
6
7
8
9
10
11
typedef struct
{
uint32_t StdId; /* 接收报文的标准 ID */
uint32_t ExtId; /* 接收报文的扩展 ID */
uint8_t IDE; /* 报文的 IDE 位 */
uint8_t RTR; /* 报文的 RTR 位 */
uint8_t DLC; /* 报文的 DLC 段 */
uint8_t Data[8]; /* 报文的数据段 */
uint8_t FMI; /* 过滤器匹配序号 */

} CanRxMsg;

接收报文、编写中断服务函数

从机中断服务函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void USB_LP_CAN1_RX0_IRQHandler(void)
{
CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);

/* 比较是否是发送的数据和 ID */

if ((RxMessage.ExtId==0x1314) && (RxMessage.IDE==CAN_ID_EXT)&& (RxMessage.DLC==2) \
&&((RxMessage.Data[1]|RxMessage.Data[0]<<8)==0xABCD))
{
flag = 0; //接收成功
}
else
{
flag = 0xff; //接收失败
}
}

在中断服务函数中,我们调用了库函数 CAN_Receive() 把 FIFO0 的报文读取到 main
文件的 CanRxMsg 类型全局变量 RxMessage 中。

使用 CAN_Receive() 函数接收了报文后,我们使用 if 语句判断接收到的报文的 ID 信息、IDE 位、DLC 位及数据段是否等于 0xABCD,若接收到的报文与我们主机预定发送的报文一样,则把 fl ag 位置“0”,退出中断服务函数。